﻿using Jint;
using Jint.Native;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.Linq;

namespace CK.Sprite.Core
{
    public static class JavascriptUtil
    {
        public static T Execute<T>(string expression, Dictionary<string, object> args)
        {
            Engine engine = new Engine();
            SetParams(engine, args);

            engine.Execute(expression);
            return (T)ConvertValue(engine.GetCompletionValue(), typeof(T));
        }

        private static void SetParams(Engine engine, Dictionary<string, object> args)
        {
            foreach(var arg in args)
            {
                engine.SetValue(arg.Key, arg.Value);
            }
        }

        private static object ConvertValue(JsValue value, Type targetType)
        {
            if (value.IsUndefined())
                return null;

            if (value.IsNull())
                return default;

            var targetIsObject = targetType == typeof(object);

            if (value.IsBoolean())
                return value.AsBoolean();

            if ((targetIsObject || targetType == typeof(DateTime)) && value.IsDate())
                return value.AsDate().ToDateTime();

            if ((targetIsObject || targetType.IsNumeric()) && value.IsNumber())
                return Convert.ChangeType(value.AsNumber(), targetType);

            if (targetType == typeof(string))
                return value.ToString();

            if (value.IsString())
            {
                var stringValue = value.AsString();
                return targetType != null
                    ? targetType == typeof(Uri)
                        ? new Uri(stringValue, UriKind.RelativeOrAbsolute)
                        : Convert.ChangeType(stringValue, targetType)
                    : value.AsString();
            }

            if (value.IsArray())
            {
                var arrayInstance = value.AsArray();
                var elementType = targetType?.GetElementType() ?? targetType?.GenericTypeArguments?.First() ?? typeof(object);

                if (elementType == typeof(byte))
                {
                    var bytes = new byte[arrayInstance.Length];

                    for (uint i = 0; i < arrayInstance.Length; i++)
                    {
                        var jsValue = arrayInstance[i];
                        bytes[i] = (byte)jsValue.AsNumber();
                    }

                    return bytes;
                }

                var array = Array.CreateInstance(elementType, arrayInstance.Length);

                for (uint i = 0; i < array.Length; i++)
                {
                    var jsValue = arrayInstance[i];
                    var convertedValue = ConvertValue(jsValue, elementType);
                    array.SetValue(convertedValue, i);
                }

                return array;
            }

            if (value.IsObject())
            {
                var obj = value.AsObject().ToObject();

                switch (obj)
                {
                    case ExpandoObject _:
                        {
                            var json = JsonConvert.SerializeObject(obj);
                            return JsonConvert.DeserializeObject(json, targetType);
                        }
                    case JValue jValue:
                        return jValue.Value;
                    default:
                        return obj;
                }
            }

            throw new ArgumentException($"Value type {value.Type} is not supported.", nameof(value));
        }
    }

    public static class TypeExtensions
    {
        private static readonly HashSet<Type> NumericTypes = new HashSet<Type> {
            typeof(byte),
            typeof(sbyte),
            typeof(short),
            typeof(ushort),
            typeof(int),
            typeof(uint),
            typeof(long),
            typeof(ulong),
            typeof(double),
            typeof(decimal),
            typeof(float)
        };

        public static bool IsNumeric(this Type type)
        {
            return NumericTypes.Contains(type);
        }
    }
}
